引用

  一个引用就是某对象的另一个名字。引用的主要用途是为了描述函数的参数和返回值,特别是为了运算符的重载(第11章)。记法X&表示到X的引用。例如,

    void f()
    {
        int i = 1;
        int& r = i;            // r和i现在引用同一个int
        int x = r;             // x = 1

        r = 2;                 // i = 2
    }

为了确保一个引用总能是某个东西的名字(也就是说,总能约束到某个对象),我们必须对引用做初始化。例如,

    int i = 1;
    int& r1 = i;            // 正确:r1被初始化
    int& r2;                // 错误❌:没有初始化
    extern int& r3;         // 正确:r3在别处初始化

对一个引用的初始化是与对它赋值完全不同的另一件事情。除了外表形式之外,实际上根本就没有能操作引用的运算符操作。例如,

    void g()
    {
        int ii = 0;
        int& rr = ii;
        rr++;                 // ii被增加1
        int* pp = &rr;        // pp指向ii
    }

这些都合法,但是rr++并没有对引用本身做什么增量操作;相反,++是应用到了那个int上,而这个int碰巧就是ii。因此,一个引用的值在初始化之后就不可能改变了,它总是引用它的初始化所指称的那个对象。要取得被引用rr所引用对象的地址,我们可以写&rr。

  引用的一种最明显的实现方式是作为一个(常量)指针,在每次使用它时都自动地做间接访问。将引用想象成这种样子不会有任何问题,但要记住的是,一个引用并不是一个对象,不能像指针那样去操作。

在一些情况下,编译器可以通过优化去掉引用,使得在执行时根本不存在任何表示引用的东西。

  当引用的初始式是一个左值时(是一个对象,你可以取得它的地址,见4.9.6节),其初始化就是非常简单的事情。对“普通”T&的初始式必须是一个类型T的左值。

对一个const T&的初始式不必是一个左值,甚至可以不是类型T的;在这种情况下:

1)、首先,如果需要将应用到T的隐式类型转换(见C.6节)。

2)、而后将结果存入一个类型T的临时变量。

3)、最后,将此临时变量用做初始式的值。

考虑

    double& dr = 1;               // 错误❌:要求左值
    const double& cdr = 1;        // ok

对后一个初始化的解释是

    double temp = double(1);        // 首先建立一个具有正确值的临时变量
    const double& cdr = temp;       // 而后用这个临时变量作为cdr的初始式

这种保存引用初始式的临时变量将一直存在,直到这个引用的作用域结束。

  需要区分对变量的引用和对常量的引用,是因为在变量引用的情况下引进临时量极易出错,对变量的赋值将会变成对于---即将消失的---临时量的赋值。对于常量引用则不会有这类问题,以常量的引用作为函数参数经常是很重要的(11.6节)。

  可以通过引用来描述一个函数参数,以使该函数能够改变传递来的变量的值。例如,

    void increment(int& aa) { aa++; }
    void f()
    {
        int x = 1;
        increment(x);        // x = 2
    }

参数传递的语义通过对应的初始化定义,所以,在调用时,increament的参数aa将变成x的另一个名字。为了提高程序的可读性,通常应该尽可能避免让函数去修改它们的参数。相反,你应该让函数明确地返回一个值,或者明确要求一个指针参数:

    int next(int p) { return p + 1; } 
    void incr(int* p) { (*p)++; }
    void g()
    {
        int x = 1;
        increment(x);        // x = 2
        x = next(x);         // x = 3
        incr(&x);            // x = 4
    }

increament(x)的记法形式不能给读程序的人有关x的值可能被修改的提示性信息。而采用x=next(x)和incr(&x)的形式则可以。因此,如果将“普通”引用参数用于某些函数,那么这些函数的名字就应该给出其引用参数将被修改的强烈提示。

  引用还可以用于定义一些函数,使它们既可以被用在赋值的左边,也可以用在右边。同样,许多最有意思的这类应用可以在比较复杂的用户定义类型中找到。作为一个例子,让我们定义一个简单的关联数组。首先,我们定义结构Pair如下:

    struct Pair
    {
        string name;
        double val;
    };

基础想法就是让每个string有一个关联于它的浮点值。很容易定义一个函数value(),让它维护一个Pair的结构数据,由曾经提供给它的所有不同的字符串组成。为缩短这个演示,我们在这里采用一个非常简单的(且低效的)实现:

    vector<Pair> pairs;
    double& value(const string& s)
    /*
        维护Pair的一个集合;
        检索s,如果找到就返回其值;否则做一个新Pair并返回默认值0
    */
    {
        for(int i = 0; i < pairs.size(); i++)
            if(s == pairs[i].name) return pairs[i].val;

        Pair p = { s, 0 };
        pairs.push_back(p);    // 将Pair加到最后(3.7.3节)

        return pairs[pairs.size() - 1].val;
    }

这个功能可以被理解为一个浮点值数组,它以字符串作为下标。对于给定的参数串,value()找到对应的浮点对象(而不是对应浮点对象的值),返回到这个对象的一个引用。例如,

    int main()    // 统计每个单词在输入中出现的次数
    {
        string buf;
        while(cin>>buf) value(buf)++;
        for(vector<Pair>::const_iterator p = pairs.begin(); p!= pairs.end(); ++p)
            cout << p->name << ": " << p->val << '\n';
    }

每一次while循环从标准输入流cin将一个单词读进buf(3.6节),并更新与它关联的计数器。最后,结果的表里是输入中遇到的所有互不相同的词,最后将它们及其出现的次数打印出来。举例来说,给定输入

aa bb bb aa aa bb aa aa

程序将产生出

aa: 5
bb: 3

通过使用模板类,很容易将这个程序进一步精化为一个真正的关联数组类型,带有重载的下标运算符[](11.8节)。利用标准库的map(17.4.1节)做这件事就更容易了。

🔚